@vdfor/confluence-sync 0.1.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @vdfor/confluence-sync
2
2
 
3
- Confluence 文档同步 CLI 工具,支持将本地 Markdown 文件上传至 Confluence 并自动转换为 Confluence 页面格式。
3
+ Confluence 文档同步 CLI 工具,支持将本地 Markdown 文件上传至 Confluence 并自动转换为 Confluence 页面格式。支持 Mermaid 图表自动渲染为图片。
4
4
 
5
5
  ## 安装
6
6
 
@@ -20,7 +20,7 @@ yarn global add @vdfor/confluence-sync
20
20
  ### 方式1:环境变量
21
21
 
22
22
  ```bash
23
- # 必填:Confluence 服务地址
23
+ # Confluence 服务地址
24
24
  export CONFLUENCE_BASE_URL="https://your-confluence-instance.com"
25
25
 
26
26
  # 认证方式(二选一)
@@ -35,23 +35,23 @@ export CONFLUENCE_API_TOKEN="your-api-token"
35
35
  ### 方式2:CLI 参数(优先于环境变量)
36
36
 
37
37
  ```bash
38
- confluence-sync upload --file test.md \
38
+ confluence-sync upload --file test.md --parent 123456 \
39
39
  --base-url https://your-confluence-instance.com \
40
40
  --pat your-token
41
41
  ```
42
42
 
43
43
  ## 命令
44
44
 
45
- ### upload - 上传 Markdown 为新页面
45
+ ### upload - 上传 Markdown 为新页面(已存在同名页面则自动更新)
46
46
 
47
47
  ```bash
48
- confluence-sync upload --file <path> [--title <标题>] [--space <key>] [--parent <ID或标题>] [--dry-run]
48
+ confluence-sync upload --file <path> --parent <页面ID> [--title <标题>] [--dry-run] [--no-mermaid-to-img]
49
49
  ```
50
50
 
51
51
  ### update - 更新已有页面
52
52
 
53
53
  ```bash
54
- confluence-sync update --file <path> (--page-id <ID> | --title <标题>) [--space <key>] [--minor-edit]
54
+ confluence-sync update --file <path> --page-id <ID> [--minor-edit] [--no-mermaid-to-img]
55
55
  ```
56
56
 
57
57
  ### upload-file - 上传附件
@@ -60,10 +60,10 @@ confluence-sync update --file <path> (--page-id <ID> | --title <标题>) [--spac
60
60
  confluence-sync upload-file --page-id <ID> --file <path> [--name <文件名>]
61
61
  ```
62
62
 
63
- ### list-pages - 列出空间页面
63
+ ### list-pages - 列出 parent 下的子页面
64
64
 
65
65
  ```bash
66
- confluence-sync list-pages [--space <key>]
66
+ confluence-sync list-pages --parent <页面ID>
67
67
  ```
68
68
 
69
69
  ### list-files - 列出页面附件
@@ -80,28 +80,44 @@ confluence-sync get-content --page-id <ID> [--markdown]
80
80
 
81
81
  使用 `--markdown` 参数可将 Confluence 内容转换为 Markdown 格式输出。
82
82
 
83
+ ## Mermaid 图表支持
84
+
85
+ Markdown 中的 ```` ```mermaid ```` 代码块会自动通过 [sebastianjs](https://github.com/creadri/sebastianjs) 渲染为 PNG 图片(本地离线渲染,无需外网、无需浏览器),上传后页面中显示为图片附件。
86
+
87
+ - 默认自动检测并转换,无 Mermaid 块时不影响正常流程
88
+ - Mermaid 渲染失败时保留原始代码块(降级处理,不阻塞上传)
89
+ - 使用 `--no-mermaid-to-img` 可禁用自动渲染
90
+
91
+ ```bash
92
+ # 自动处理 mermaid(默认开启)
93
+ confluence-sync upload --file README.md --parent 27263539
94
+
95
+ # 禁用 mermaid 渲染
96
+ confluence-sync upload --file README.md --parent 27263539 --no-mermaid-to-img
97
+ ```
98
+
83
99
  ## 配置参数
84
100
 
85
101
  | 参数 | 说明 |
86
102
  |------|------|
87
- | `--base-url` | Confluence 服务地址 |
88
- | `--pat` | Personal Access Token |
89
- | `--username` | Confluence 登录名 |
90
- | `--api-token` | API Token 或密码 |
91
- | `--space` | Space Key |
103
+ | `--base-url` | Confluence 服务地址(覆盖环境变量) |
104
+ | `--pat` | Personal Access Token(覆盖环境变量) |
105
+ | `--username` | Confluence 登录名(覆盖环境变量) |
106
+ | `--api-token` | API Token 或密码(覆盖环境变量) |
92
107
  | `--file` | 本地文件路径 |
93
- | `--title` | 页面标题 |
94
- | `--page-id` | 页面 ID |
95
- | `--parent` | 父页面 ID 或标题 |
108
+ | `--title` | 页面标题(upload 时可选,默认取文件名) |
109
+ | `--parent` | 父页面 ID(数字,upload / list-pages 必填) |
110
+ | `--page-id` | 页面 ID(update / upload-file / list-files / get-content 必填) |
96
111
  | `--dry-run` | 仅打印转换结果,不实际上传 |
97
112
  | `--minor-edit` | 标记为小修改(不通知关注者) |
98
113
  | `--markdown` | get-content 时输出为 Markdown 格式 |
114
+ | `--no-mermaid-to-img` | 禁用 Mermaid 图表自动渲染 |
99
115
 
100
116
  ## 示例
101
117
 
102
118
  ```bash
103
- # 上传 Markdown 文件
104
- confluence-sync upload --file README.md --title "项目文档" --space MYSPACE
119
+ # 上传 Markdown 文件(自动按 title 匹配 parent 下已有页面则更新)
120
+ confluence-sync upload --file README.md --title "项目文档" --parent 27263539
105
121
 
106
122
  # 更新已有页面
107
123
  confluence-sync update --file README.md --page-id 123456
@@ -109,8 +125,8 @@ confluence-sync update --file README.md --page-id 123456
109
125
  # 上传附件
110
126
  confluence-sync upload-file --page-id 123456 --file image.png
111
127
 
112
- # 列出空间所有页面
113
- confluence-sync list-pages --space MYSPACE
128
+ # 列出 parent 下的子页面
129
+ confluence-sync list-pages --parent 27263539
114
130
 
115
131
  # 获取页面内容
116
132
  confluence-sync get-content --page-id 123456
@@ -119,6 +135,26 @@ confluence-sync get-content --page-id 123456
119
135
  confluence-sync get-content --page-id 123456 --markdown
120
136
  ```
121
137
 
138
+ ## Skill 集成
139
+
140
+ 本项目包含 Trae Skill,安装后可被智能体调用。
141
+
142
+ ### 安装
143
+
144
+ ```bash
145
+ pnpm add -g @vdfor/confluence-sync
146
+ ```
147
+
148
+ ### 使用
149
+
150
+ 安装后,智能体可直接调用 `confluence-sync` 命令:
151
+
152
+ ```bash
153
+ confluence-sync upload --file README.md --title "项目文档" --parent 27263539
154
+ confluence-sync update --file README.md --page-id 123456
155
+ confluence-sync get-content --page-id 123456 --markdown
156
+ ```
157
+
122
158
  ## License
123
159
 
124
160
  MIT
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vdfor/confluence-sync",
3
- "version": "0.1.0",
3
+ "version": "0.4.0",
4
4
  "author": "vdfor",
5
5
  "license": "MIT",
6
6
  "private": false,
@@ -20,7 +20,8 @@
20
20
  },
21
21
  "files": [
22
22
  "upload.mjs",
23
- "src/"
23
+ "src/",
24
+ "skills/"
24
25
  ],
25
26
  "engines": {
26
27
  "node": ">=18"
@@ -28,7 +29,8 @@
28
29
  "dependencies": {
29
30
  "axios": "^1.7.0",
30
31
  "form-data": "^4.0.0",
31
- "marked": "^15.0.0"
32
+ "marked": "^15.0.0",
33
+ "sebastianjs": "^0.4.0"
32
34
  },
33
35
  "devDependencies": {
34
36
  "rimraf": "^6.1.3"
@@ -0,0 +1,97 @@
1
+ ---
2
+ name: "confluence-sync"
3
+ description: "Confluence 文档同步 CLI 工具,支持将本地 Markdown 文件上传至 Confluence 并自动转换为 Confluence 页面格式,支持 Mermaid 图表自动渲染。当用户需要上传、更新、获取 Confluence 页面内容或管理附件时调用。"
4
+ ---
5
+
6
+ # Confluence Sync
7
+
8
+ Confluence 文档同步 CLI 工具,支持将本地 Markdown 文件上传至 Confluence 并自动转换为 Confluence 页面格式。支持 Mermaid 图表自动渲染为图片。
9
+
10
+ ## 功能特性
11
+
12
+ - 上传 Markdown 文件为 Confluence 页面(已存在同名页面则自动更新)
13
+ - 更新已有页面内容
14
+ - 上传文件作为页面附件
15
+ - 列出 parent 下的子页面
16
+ - 列出页面附件
17
+ - 获取页面内容(支持转换为 Markdown 格式)
18
+ - Mermaid 图表自动渲染为 PNG 图片
19
+
20
+ ## 配置要求
21
+
22
+ 使用前需要配置以下环境变量或 CLI 参数:
23
+
24
+ - `CONFLUENCE_BASE_URL`: Confluence 服务地址
25
+ - 认证方式(二选一):
26
+ - `CONFLUENCE_PAT`: Personal Access Token(推荐)
27
+ - `CONFLUENCE_USERNAME` + `CONFLUENCE_API_TOKEN`: Basic Auth 认证
28
+
29
+ ## 命令列表
30
+
31
+ ### upload - 上传 Markdown 为新页面(已存在同名页面则自动更新)
32
+
33
+ ```bash
34
+ confluence-sync upload --file <path> --parent <页面ID> [--title <标题>] [--dry-run] [--no-mermaid-to-img]
35
+ ```
36
+
37
+ ### update - 更新已有页面
38
+
39
+ ```bash
40
+ confluence-sync update --file <path> --page-id <ID> [--minor-edit] [--no-mermaid-to-img]
41
+ ```
42
+
43
+ ### upload-file - 上传附件
44
+
45
+ ```bash
46
+ confluence-sync upload-file --page-id <ID> --file <path> [--name <文件名>]
47
+ ```
48
+
49
+ ### list-pages - 列出 parent 下的子页面
50
+
51
+ ```bash
52
+ confluence-sync list-pages --parent <页面ID>
53
+ ```
54
+
55
+ ### list-files - 列出页面附件
56
+
57
+ ```bash
58
+ confluence-sync list-files --page-id <ID>
59
+ ```
60
+
61
+ ### get-content - 获取页面内容
62
+
63
+ ```bash
64
+ confluence-sync get-content --page-id <ID> [--markdown]
65
+ ```
66
+
67
+ 使用 `--markdown` 参数可将 Confluence 内容转换为 Markdown 格式输出。
68
+
69
+ ## Mermaid 支持
70
+
71
+ Markdown 中的 ```` ```mermaid ```` 代码块自动通过 sebastianjs 渲染为 PNG 图片(本地离线渲染,无需外网、无需浏览器),作为附件上传。默认开启,使用 `--no-mermaid-to-img` 可禁用。渲染失败时保留原始代码块,不阻塞上传。
72
+
73
+ ## 使用示例
74
+
75
+ ```bash
76
+ # 上传(--parent 必填,已存在同名子页面则自动更新)
77
+ confluence-sync upload --file README.md --title "项目文档" --parent 27263539
78
+
79
+ # 更新(--page-id 必填)
80
+ confluence-sync update --file README.md --page-id 123456
81
+
82
+ # 列出 parent 下的子页面
83
+ confluence-sync list-pages --parent 27263539
84
+
85
+ # 上传附件
86
+ confluence-sync upload-file --page-id 123456 --file image.png
87
+
88
+ # 获取页面内容
89
+ confluence-sync get-content --page-id 123456 --markdown
90
+ ```
91
+
92
+ ## 注意事项
93
+
94
+ 1. 使用前请确保已正确配置认证信息
95
+ 2. Personal Access Token 需要在 Confluence 个人设置中生成
96
+ 3. 上传附件时,如果附件已存在会自动更新
97
+ 4. 使用 `--dry-run` 参数可以预览转换结果而不实际上传
@@ -1,12 +1,74 @@
1
1
  import axios from 'axios';
2
2
  import { marked } from 'marked';
3
- import { readFileSync } from 'node:fs';
4
3
  import { extname } from 'node:path';
4
+ import { tmpdir } from 'node:os';
5
+ import { writeFile, unlink } from 'node:fs/promises';
5
6
 
6
- // ── 配置 ──────────────────────────────────────────────────────────────────────
7
- const config = JSON.parse(readFileSync(new URL('../config.json', import.meta.url), 'utf-8'));
8
- const { spaceKey: defaultSpaceKey } = config;
7
+ // ── Mermaid 处理 ─────────────────────────────────────────────────────────────
8
+
9
+ let sebastianRenderer = null;
10
+ async function getSebastianRenderer() {
11
+ if (!sebastianRenderer) {
12
+ const sebastian = await import('sebastianjs');
13
+ sebastianRenderer = { renderPng: sebastian.renderPng, dispose: sebastian.dispose };
14
+ }
15
+ return sebastianRenderer;
16
+ }
17
+
18
+ /**
19
+ * 检测并替换 markdown 中的 ```mermaid 代码块为图片占位符
20
+ * 使用 sebastianjs(本地、零浏览器、零外网依赖)渲染为 PNG
21
+ *
22
+ * @param {string} md - 原始 markdown
23
+ * @returns {{ md: string, images: { file: string, name: string }[], hasMermaid: boolean }}
24
+ */
25
+ export async function preprocessMermaid(md) {
26
+ const mermaidRe = /```mermaid\s*\n([\s\S]*?)```/gi;
27
+ const matches = [...md.matchAll(mermaidRe)];
28
+ if (!matches.length) return { md, images: [], hasMermaid: false };
29
+
30
+ const { renderPng } = await getSebastianRenderer();
31
+ const images = [];
32
+ let result = md;
33
+ let offset = 0;
34
+
35
+ for (let i = 0; i < matches.length; i++) {
36
+ const block = matches[i];
37
+ const code = block[1];
38
+ const name = `mermaid-${i + 1}.png`;
39
+ const file = `${tmpdir()}/${name}`;
40
+
41
+ try {
42
+ const t0 = Date.now();
43
+ const { data: pngBuffer } = await renderPng(code, { background: 'white' });
44
+ const t1 = Date.now();
45
+ await writeFile(file, pngBuffer);
46
+ images.push({ file, name });
47
+
48
+ const replacement = `![${name}](${name})`;
49
+ const start = block.index + offset;
50
+ const end = start + block[0].length;
51
+ result = result.slice(0, start) + replacement + result.slice(end);
52
+ offset += replacement.length - block[0].length;
53
+ console.log(` 📐 Mermaid 图表 ${i + 1} 渲染完成 → ${name} (${pngBuffer.length}B, ${t1 - t0}ms)`);
54
+ } catch (err) {
55
+ console.warn(` ⚠️ Mermaid 图表 ${i + 1} 渲染失败:${err.message},保留原始代码块`);
56
+ }
57
+ }
58
+
59
+ return { md: result, images, hasMermaid: images.length > 0 };
60
+ }
61
+
62
+ /**
63
+ * 清理临时图片文件
64
+ */
65
+ async function cleanupMermaidImages(images) {
66
+ for (const img of images) {
67
+ try { await unlink(img.file); } catch {}
68
+ }
69
+ }
9
70
 
71
+ // ── 配置 ──────────────────────────────────────────────────────────────────────
10
72
  let baseUrl = process.env.CONFLUENCE_BASE_URL;
11
73
  let pat = process.env.CONFLUENCE_PAT;
12
74
  let username = process.env.CONFLUENCE_USERNAME;
@@ -62,44 +124,23 @@ function logAxiosError(err) {
62
124
  // ── 页面查询 ──────────────────────────────────────────────────────────────────
63
125
 
64
126
  /**
65
- * 按 ID 获取页面元信息(含版本号)
127
+ * 按 ID 获取页面元信息(含版本号、space)
66
128
  */
67
129
  export async function getPageById(id) {
68
130
  ensureApi();
69
131
  const { data } = await api.get(`/content/${id}`, {
70
- params: { expand: 'version,ancestors' },
132
+ params: { expand: 'version,ancestors,space' },
71
133
  });
72
134
  return data;
73
135
  }
74
136
 
75
137
  /**
76
- * spaceKey + title 获取页面(仅返回第一个匹配)
138
+ * 列出指定 parent 下的子页面
77
139
  */
78
- export async function getPageByTitle(spaceKey, title) {
140
+ export async function listChildPages(parentId, limit = 50) {
79
141
  ensureApi();
80
- const { data } = await api.get('/content', {
81
- params: {
82
- spaceKey,
83
- title,
84
- expand: 'version',
85
- limit: 1,
86
- },
87
- });
88
- return data.results?.[0] || null;
89
- }
90
-
91
- /**
92
- * 列出 space 下所有页面(分页)
93
- */
94
- export async function listPages(spaceKey, limit = 25) {
95
- ensureApi();
96
- const { data } = await api.get('/content', {
97
- params: {
98
- spaceKey,
99
- type: 'page',
100
- expand: 'version',
101
- limit,
102
- },
142
+ const { data } = await api.get(`/content/${parentId}/child/page`, {
143
+ params: { limit, expand: 'version' },
103
144
  });
104
145
  return data.results;
105
146
  }
@@ -118,19 +159,16 @@ export async function getPageContent(id) {
118
159
  // ── 页面创建 / 更新 ───────────────────────────────────────────────────────────
119
160
 
120
161
  /**
121
- * 创建新页面
162
+ * 创建新页面(有 parentId 即可,Confluence 自动推断 space)
122
163
  */
123
- export async function createPage(spaceKey, title, bodyHtml, parentId) {
164
+ export async function createPage(parentId, title, bodyHtml) {
124
165
  ensureApi();
125
166
  const payload = {
126
167
  type: 'page',
127
168
  title,
128
- space: { key: spaceKey },
129
169
  body: { storage: { value: bodyHtml, representation: 'storage' } },
170
+ ancestors: parentId ? [{ id: parentId }] : undefined,
130
171
  };
131
- if (parentId) {
132
- payload.ancestors = [{ id: parentId }];
133
- }
134
172
  try {
135
173
  const { data } = await api.post('/content', payload);
136
174
  return data;
@@ -449,95 +487,122 @@ export async function listAttachments(pageId) {
449
487
 
450
488
  // ── 高级业务流程 ──────────────────────────────────────────────────────────────
451
489
 
452
- /**
453
- * 查找父页面 ID(支持按 title 或直接 ID)
454
- */
455
- async function resolveParentId(parentIdentifier, spaceKey) {
456
- if (!parentIdentifier) return null;
457
- // 如果是纯数字,当作 ID
458
- if (/^\d+$/.test(parentIdentifier)) {
459
- const page = await getPageById(parentIdentifier);
460
- console.log(` 父页面:${page.title} (ID: ${page.id})`);
461
- return page.id;
462
- }
463
- // 否则按 title 查找
464
- const page = await getPageByTitle(spaceKey, parentIdentifier);
465
- if (!page) {
466
- throw new Error(`未找到父页面「${parentIdentifier}」(space: ${spaceKey})`);
467
- }
468
- console.log(` 父页面:${page.title} (ID: ${page.id})`);
469
- return page.id;
470
- }
471
-
472
490
  /**
473
491
  * 上传 Markdown 文件到 Confluence
474
492
  * @param {object} opts
475
493
  * @param {string} opts.filePath - 本地 .md 文件路径
476
494
  * @param {string} [opts.title] - 页面标题(默认取文件名)
477
- * @param {string} [opts.spaceKey] - space key
478
- * @param {string} [opts.parentId] - 父页面 ID 或标题
495
+ * @param {string} opts.parentId - 父页面 ID(数字)
479
496
  * @param {boolean} [opts.dryRun] - 仅打印转换结果
480
497
  */
481
498
  export async function uploadMarkdown(opts) {
482
- const { filePath, title: optTitle, spaceKey = defaultSpaceKey, parentId: parentRaw, dryRun = false } = opts;
499
+ const { filePath, title: optTitle, parentId: parentRaw, dryRun = false, mermaid = true } = opts;
483
500
  const fs = await import('node:fs');
484
501
  const path = await import('node:path');
485
502
 
486
- const md = fs.readFileSync(filePath, 'utf-8');
503
+ let md = fs.readFileSync(filePath, 'utf-8');
487
504
  const title = optTitle || path.basename(filePath, extname(filePath));
505
+
506
+ // 必须有 parentId
507
+ if (!parentRaw || !/^\d+$/.test(parentRaw)) {
508
+ throw new Error('upload 需要 --parent <数字页面ID> 作为父页面');
509
+ }
510
+
511
+ const parentPage = await getPageById(parentRaw);
512
+ console.log(` 父页面:${parentPage.title} (ID: ${parentPage.id}, Space: ${parentPage.space?.key})`);
513
+
514
+ // Mermaid 预处理
515
+ let mermaidImages = [];
516
+ if (mermaid) {
517
+ const result = await preprocessMermaid(md);
518
+ md = result.md;
519
+ mermaidImages = result.images;
520
+ }
521
+
488
522
  const bodyHtml = mdToConfluence(md);
489
523
 
490
524
  if (dryRun) {
491
525
  console.log('── 转换结果(dry-run)────────────────────────');
492
526
  console.log(` 标题: ${title}`);
493
- console.log(` Space: ${spaceKey}`);
527
+ console.log(` 父页面: ${parentPage.title} (ID: ${parentPage.id})`);
528
+ if (mermaidImages.length) console.log(` Mermaid 图片: ${mermaidImages.length} 张`);
494
529
  console.log(' Body:\n' + bodyHtml);
495
530
  return null;
496
531
  }
497
532
 
498
- // 查找父页面
499
- const parentId = await resolveParentId(parentRaw, spaceKey);
533
+ try {
534
+ // 检查 parent 下是否已有同名子页面
535
+ const children = await listChildPages(parentPage.id, 500);
536
+ const existing = children.find(c => c.title === title);
537
+
538
+ let page;
539
+ if (existing) {
540
+ console.log(` 父页面下已存在同名页面「${title}」(ID: ${existing.id}),将更新…`);
541
+ page = await updatePage(existing.id, title, bodyHtml, existing.version.number);
542
+ } else {
543
+ page = await createPage(parentPage.id, title, bodyHtml);
544
+ }
500
545
 
501
- // 检查是否已存在同名页面
502
- const existing = await getPageByTitle(spaceKey, title);
503
- if (existing) {
504
- console.log(` 页面「${title}」已存在 (ID: ${existing.id}),将更新…`);
505
- const updated = await updatePage(existing.id, title, bodyHtml, existing.version.number);
506
- console.log(` 更新完成 → ${baseUrl}/pages/viewpage.action?pageId=${updated.id}`);
507
- return updated;
508
- }
546
+ // 上传 mermaid 图片作为附件
547
+ if (mermaidImages.length) {
548
+ console.log(' 📎 上传 Mermaid 图片附件…');
549
+ for (const img of mermaidImages) {
550
+ await uploadAttachment(page.id, img.file, img.name);
551
+ console.log(` ✅ ${img.name}`);
552
+ }
553
+ }
509
554
 
510
- const page = await createPage(spaceKey, title, bodyHtml, parentId);
511
- console.log(` ✅ 创建完成 → ${baseUrl}/pages/viewpage.action?pageId=${page.id}`);
512
- return page;
555
+ console.log(` ${existing ? '更新' : '创建'}完成 ${baseUrl}/pages/viewpage.action?pageId=${page.id}`);
556
+ return page;
557
+ } finally {
558
+ await cleanupMermaidImages(mermaidImages);
559
+ }
513
560
  }
514
561
 
515
562
  /**
516
- * 更新指定页面(通过 ID 或 space+title 查找)
563
+ * 更新指定页面(通过 page-id 定位)
517
564
  */
518
565
  export async function updateMarkdown(opts) {
519
- const { pageId, title: searchTitle, filePath, spaceKey = defaultSpaceKey, minorEdit = false } = opts;
566
+ const { pageId, filePath, minorEdit = false, mermaid = true } = opts;
520
567
  const fs = await import('node:fs');
521
568
 
522
- const md = fs.readFileSync(filePath, 'utf-8');
523
- const bodyHtml = mdToConfluence(md);
569
+ if (!pageId || !/^\d+$/.test(pageId)) {
570
+ throw new Error('update 需要 --page-id <数字页面ID>');
571
+ }
572
+
573
+ let md = fs.readFileSync(filePath, 'utf-8');
524
574
 
525
- let page;
526
- if (pageId) {
527
- page = await getPageById(pageId);
528
- } else if (searchTitle) {
529
- page = await getPageByTitle(spaceKey, searchTitle);
530
- if (!page) throw new Error(`未找到页面「${searchTitle}」(space: ${spaceKey})`);
531
- } else {
532
- throw new Error('必须指定 --page-id 或 --title 来定位目标页面');
575
+ // Mermaid 预处理
576
+ let mermaidImages = [];
577
+ if (mermaid) {
578
+ const result = await preprocessMermaid(md);
579
+ md = result.md;
580
+ mermaidImages = result.images;
533
581
  }
534
582
 
535
- console.log(` 目标页面:${page.title} (ID: ${page.id}, 当前版本: ${page.version.number})`);
536
- const updated = await updatePage(page.id, page.title, bodyHtml, page.version.number, minorEdit);
537
- console.log(` ✅ 更新完成 ${baseUrl}/pages/viewpage.action?pageId=${updated.id}`);
538
- return updated;
583
+ const bodyHtml = mdToConfluence(md);
584
+
585
+ const page = await getPageById(pageId);
586
+
587
+ try {
588
+ console.log(` 目标页面:${page.title} (ID: ${page.id}, Space: ${page.space?.key}, 当前版本: ${page.version.number})`);
589
+ const updated = await updatePage(page.id, page.title, bodyHtml, page.version.number, minorEdit);
590
+
591
+ // 上传 mermaid 图片作为附件
592
+ if (mermaidImages.length) {
593
+ console.log(' 📎 上传 Mermaid 图片附件…');
594
+ for (const img of mermaidImages) {
595
+ await uploadAttachment(page.id, img.file, img.name);
596
+ console.log(` ✅ ${img.name}`);
597
+ }
598
+ }
599
+
600
+ console.log(` ✅ 更新完成 → ${baseUrl}/pages/viewpage.action?pageId=${updated.id}`);
601
+ return updated;
602
+ } finally {
603
+ await cleanupMermaidImages(mermaidImages);
604
+ }
539
605
  }
540
606
 
541
607
  // ── 导出配置 ──────────────────────────────────────────────────────────────────
542
- export { defaultSpaceKey };
543
608
  export function getBaseUrl() { return baseUrl; }
package/upload.mjs CHANGED
@@ -2,37 +2,36 @@
2
2
  /**
3
3
  * Confluence 文档同步 CLI
4
4
  *
5
- * 环境变量(或通过 CLI 参数配置):
6
- * 必填: CONFLUENCE_BASE_URL - Confluence 服务地址
7
- * 认证方式(二选一):
8
- * 方式1 PAT: CONFLUENCE_PAT - Personal Access Token
9
- * 方式2 Basic Auth: CONFLUENCE_USERNAME - Confluence 登录名(工号)
10
- * CONFLUENCE_API_TOKEN - API Token 或密码
5
+ * 环境变量:
6
+ * CONFLUENCE_BASE_URL - Confluence 服务地址
7
+ * 认证方式(二选一):
8
+ * 方式1 PAT: CONFLUENCE_PAT - Personal Access Token
9
+ * 方式2 Auth: CONFLUENCE_USERNAME + CONFLUENCE_API_TOKEN
11
10
  *
12
11
  * 用法:
13
- * confluence-sync upload --file <path> [--title <标题>] [--space <key>] [--parent <ID或标题>] [--dry-run]
14
- * confluence-sync update --file <path> (--page-id <ID> | --title <标题>) [--space <key>] [--minor-edit]
12
+ * confluence-sync upload --file <path> --parent <页面ID> [--title <标题>] [--dry-run] [--no-mermaid-to-img]
13
+ * confluence-sync update --file <path> --page-id <ID> [--minor-edit] [--no-mermaid-to-img]
15
14
  * confluence-sync upload-file --page-id <ID> --file <path> [--name <文件名>]
16
- * confluence-sync list-pages [--space <key>]
17
- * confluence-sync list-files --page-id <ID>
15
+ * confluence-sync list-pages --parent <页面ID>
16
+ * confluence-sync list-files --page-id <ID>
18
17
  * confluence-sync get-content --page-id <ID> [--markdown]
19
18
  *
20
19
  * 可选配置参数(优先于环境变量):
21
- * --base-url Confluence 服务地址
22
- * --pat Personal Access Token
23
- * --username Confluence 登录名
24
- * --api-token API Token 或密码
20
+ * --base-url Confluence 服务地址
21
+ * --pat Personal Access Token
22
+ * --username Confluence 登录名
23
+ * --api-token API Token 或密码
25
24
  */
26
25
 
27
26
  import {
28
27
  uploadMarkdown,
29
28
  updateMarkdown,
30
29
  uploadAttachment,
31
- listPages,
30
+ listChildPages,
32
31
  listAttachments,
32
+ getPageById,
33
33
  getPageContent,
34
34
  confluenceToMd,
35
- defaultSpaceKey,
36
35
  getBaseUrl,
37
36
  configure,
38
37
  } from './src/confluence.mjs';
@@ -71,23 +70,22 @@ Confluence 文档同步工具
71
70
  upload 上传 Markdown 文件为新页面(已存在则更新)
72
71
  update 更新已有页面内容
73
72
  upload-file 上传文件作为页面附件
74
- list-pages 列出 space 下所有页面
73
+ list-pages 列出 parent 页面下的子页面
75
74
  list-files 列出页面的所有附件
76
75
  get-content 获取页面的 HTML 内容
77
76
 
78
77
  upload 选项:
79
78
  --file 本地 .md 文件路径(必填)
79
+ --parent 父页面 ID(数字,必填)
80
80
  --title 页面标题(默认取文件名)
81
- --space Space Key(默认读 config.json)
82
- --parent 父页面 ID 或标题
83
81
  --dry-run 仅打印转换结果,不实际上传
82
+ --no-mermaid-to-img 禁用 Mermaid 图表自动渲染
84
83
 
85
84
  update 选项:
86
85
  --file 本地 .md 文件路径(必填)
87
- --page-id 目标页面 ID
88
- --title 按标题查找目标页面(与 --page-id 二选一)
89
- --space Space Key(默认读 config.json)
86
+ --page-id 目标页面 ID(数字,必填)
90
87
  --minor-edit 标记为小修改(不通知关注者)
88
+ --no-mermaid-to-img 禁用 Mermaid 图表自动渲染
91
89
 
92
90
  upload-file 选项:
93
91
  --page-id 目标页面 ID(必填)
@@ -95,32 +93,26 @@ upload-file 选项:
95
93
  --name 覆盖文件名
96
94
 
97
95
  list-pages 选项:
98
- --space Space Key(默认读 config.json)
96
+ --parent 父页面 ID(数字,必填)
99
97
 
100
98
  list-files 选项:
101
99
  --page-id 页面 ID(必填)
102
100
 
103
101
  get-content 选项:
104
102
  --page-id 页面 ID(必填)
105
- --markdown 输出为 Markdown 格式(默认输出 Storage HTML)
103
+ --markdown 输出为 Markdown 格式
106
104
 
107
105
  配置参数(优先于环境变量):
108
- --base-url Confluence 服务地址
109
- --pat Personal Access Token
110
- --username Confluence 登录名
111
- --api-token API Token 或密码
106
+ --base-url Confluence 服务地址
107
+ --pat Personal Access Token
108
+ --username Confluence 登录名
109
+ --api-token API Token 或密码
112
110
 
113
111
  环境变量:
114
- 必填:
115
- CONFLUENCE_BASE_URL Confluence 服务地址
116
-
112
+ CONFLUENCE_BASE_URL Confluence 服务地址
117
113
  认证方式(二选一):
118
- 方式1 - Personal Access Token:
119
- CONFLUENCE_PAT 个人访问令牌
120
-
121
- 方式2 - Basic Auth:
122
- CONFLUENCE_USERNAME Confluence 登录名(工号)
123
- CONFLUENCE_API_TOKEN API Token 或密码
114
+ CONFLUENCE_PAT Personal Access Token
115
+ CONFLUENCE_USERNAME + CONFLUENCE_API_TOKEN Basic Auth
124
116
 
125
117
  `);
126
118
  }
@@ -155,9 +147,9 @@ async function main() {
155
147
  const result = await uploadMarkdown({
156
148
  filePath: args.file,
157
149
  title: args.title,
158
- spaceKey: args.space || defaultSpaceKey,
159
150
  parentId: args.parent,
160
151
  dryRun: args['dry-run'] === true,
152
+ mermaid: args['no-mermaid-to-img'] !== true,
161
153
  });
162
154
  if (result) {
163
155
  console.log('\n页面信息:');
@@ -174,16 +166,15 @@ async function main() {
174
166
  console.error('错误:--file 必填');
175
167
  process.exit(1);
176
168
  }
177
- if (!args['page-id'] && !args.title) {
178
- console.error('错误:必须指定 --page-id 或 --title');
169
+ if (!args['page-id']) {
170
+ console.error('错误:--page-id 必填');
179
171
  process.exit(1);
180
172
  }
181
173
  const result = await updateMarkdown({
182
174
  pageId: args['page-id'],
183
- title: args.title,
184
175
  filePath: args.file,
185
- spaceKey: args.space || defaultSpaceKey,
186
176
  minorEdit: args['minor-edit'] === true,
177
+ mermaid: args['no-mermaid-to-img'] !== true,
187
178
  });
188
179
  console.log('\n页面信息:');
189
180
  console.log(` ID: ${result.id}`);
@@ -210,18 +201,22 @@ async function main() {
210
201
  }
211
202
 
212
203
  case 'list-pages': {
213
- const space = args.space || defaultSpaceKey;
214
- console.log(`Space: ${space}\n`);
215
- const pages = await listPages(space);
204
+ if (!args.parent || !/^\d+$/.test(args.parent)) {
205
+ console.error('错误:--parent <数字页面ID> 必填');
206
+ process.exit(1);
207
+ }
208
+ const parentPage = await getPageById(args.parent);
209
+ console.log(`父页面:${parentPage.title} (ID: ${parentPage.id}, Space: ${parentPage.space?.key})\n`);
210
+ const pages = await listChildPages(parentPage.id);
216
211
  if (!pages.length) {
217
- console.log(' (无页面)');
212
+ console.log(' (无子页面)');
218
213
  } else {
219
214
  const maxId = Math.max(...pages.map(p => String(p.id).length));
220
215
  for (const p of pages) {
221
216
  const id = String(p.id).padEnd(maxId);
222
217
  console.log(` ${id} v${p.version?.number ?? '?'} ${p.title}`);
223
218
  }
224
- console.log(`\n共 ${pages.length} 个页面`);
219
+ console.log(`\n共 ${pages.length} 个子页面`);
225
220
  }
226
221
  break;
227
222
  }